#!/usr/bin/env python
# coding=utf-8
# version: V0.2

import os
import sys
import shutil
import chardet
import re
from pathlib import Path
import xml.etree.ElementTree as ET
from configparser import ConfigParser

PLATFORMIO_NAME = 'platformio.ini'
exec_path = None

def generate_platformio_ini(envs):
    print("-> Generate platformio.ini")
    cf = ConfigParser()
    cf.add_section('platformio')
    cf.set('platformio', 'include_dir', '.')
    cf.set('platformio', 'src_dir', '.')
    for env in envs:
        section = 'env:%s' % (env['name'])
        cf.add_section(section)
        cf.set(section, 'platform', env['platform'])
        if 'framework' in env:
            cf.set(section, 'framework', env['framework'])
        cf.set(section, 'board', env['board'])
        build_flags = ''
        if 'args' in env:
            for arg in env['args']:
                build_flags += os.linesep + arg
        if 'incs' in env:
            for include in env['incs']:
                build_flags += os.linesep + '-I%s' % (include)
        if 'defs' in env:
            for define in env['defs']:
                build_flags += os.linesep + '-D%s' % (define)
        cf.set(section, 'build_flags', build_flags)
        build_src_filter = ''
        build_src_filter += os.linesep + '-<*>'
        for source in env['srcs']:
            build_src_filter += os.linesep + '+<%s>' % (source)
        cf.set(section, 'build_src_filter', build_src_filter)
    with open(PLATFORMIO_NAME, 'w') as f:
        cf.write(f)

def assembly_file_replace(env):
    uccpu_dict = {
        'name': 'uC-CPU',
        'version_pattern': '(Version)( *)(:)( *)(V)([0-9]*)(\.)([0-9]*)(\.)([0-9]*)',
        'file_path': {
            'uC-CPU/ARM-Cortex-M/ARMv7-M/ARM/cpu_a.asm': 'uC-CPU/ARM-Cortex-M/ARMv7-M/GNU/cpu_a.s',
        },
        'dir_path': {
            'uC-CPU/ARM-Cortex-M/ARMv7-M/ARM': 'uC-CPU/ARM-Cortex-M/ARMv7-M/GNU',
        }
    }
    ucos2_dict = {
        'name': 'uC-OS2',
        'version_pattern': '(Version)( *)(:)( *)(V)([0-9]*)(\.)([0-9]*)(\.)([0-9]*)',
        'file_path': {
            'uC-OS2/Ports/ARM-Cortex-M/ARMv7-M/ARM/os_cpu_a.asm': 'uC-OS2/Ports/ARM-Cortex-M/ARMv7-M/GNU/os_cpu_a.S',
        },
        'dir_path': {
            'uC-OS2/Ports/ARM-Cortex-M/ARMv7-M/ARM': 'uC-OS2/Ports/ARM-Cortex-M/ARMv7-M/GNU',
        }
    }
    ucos3_dict = {
        'name': 'uC-OS3',
        'version_pattern': '(Version)( *)(:)( *)(V)([0-9]*)(\.)([0-9]*)(\.)([0-9]*)',
        'file_path': {
            'uC-OS3/Ports/ARM-Cortex-M/ARMv7-M/ARM/os_cpu_a.asm': 'uC-OS3/Ports/ARM-Cortex-M/ARMv7-M/GNU/os_cpu_a.S',
        },
        'dir_path': {
            'uC-OS3/Ports/ARM-Cortex-M/ARMv7-M/ARM': 'uC-OS3/Ports/ARM-Cortex-M/ARMv7-M/GNU',
        }
    }
    freertos_dict = {
        'name': 'FreeRTOS',
        'version_pattern': '(FreeRTOS)(.*)(V)([0-9]*)(\.)([0-9]*)(\.)([0-9]*)',
        'file_path': {},
        'dir_path': {
            'FreeRTOS/portable/RVDS/ARM_CM3': 'FreeRTOS/portable/GCC/ARM_CM3',
            'FreeRTOS/portable/RVDS/ARM_CM3_MPU': 'FreeRTOS/portable/GCC/ARM_CM3_MPU',
            'FreeRTOS/portable/RVDS/ARM_CM4F': 'FreeRTOS/portable/GCC/ARM_CM4F',
            'FreeRTOS/portable/RVDS/ARM_CM4_MPU': 'FreeRTOS/portable/GCC/ARM_CM4_MPU',
        }
    }
    assembly_list = [uccpu_dict, ucos2_dict, ucos3_dict, freertos_dict]
    global exec_path
    for asm in assembly_list:
        for s, d in asm['dir_path'].items():
            for src in env['srcs']:
                if s in src:
                    version = None
                    with open(src, 'r') as f:
                        p =re.compile(asm['version_pattern'])
                        for line in f.readlines():
                            str = p.search(line)
                            if str:
                                version = str.group().split()[-1]
                    if not version:
                        print('Error: Unknow %s version' % (asm['name']))
                        sys.exit(1)
                    src_dir = exec_path.joinpath(asm['name']).joinpath(version).joinpath(d)
                    if not src_dir.exists():
                        print('Error: Not exists %s' % (src_dir))
                        sys.exit(1)
                    dst_dir = Path(src.split(s)[0]).joinpath(d)
                    if not dst_dir.exists():
                        print('copy %s to %s' % (src_dir, dst_dir))
                        shutil.copytree(src_dir, dst_dir)
                    break
        for s, d in asm['file_path'].items():
            for i, src in enumerate(env['srcs']):
                if s in src:
                    env['srcs'][i] = src.replace(s, d)
        for s, d in asm['dir_path'].items():
            for i, src in enumerate(env['srcs']):
                if s in src:
                    env['srcs'][i] = src.replace(s, d)
            for i, inc in enumerate(env['incs']):
                if s in inc:
                    env['incs'][i] = inc.replace(s, d)

def keil5_arm_ads(target, env):
    device_dict = { 'ARMCM3': 'genericSTM32F103CB',
                    'ARMCM4': 'genericSTM32F407ZE' }
    device = target.find('TargetOption/TargetCommonOption/Device').text
    if device in device_dict:
        device = device_dict[device]
    if 'STM32F' in device:
        inc_filter = ['CMSIS']
        def_filter = ['STM32F']
        src_group_filter = ['Startup', 'Readme', 'CMSIS', 'DOC']
        src_file_filter = ['CMSIS']
        extra_defs = ['__nop=__NOP']
        # platformio
        env['platform'] = 'ststm32'
        env['framework'] = 'cmsis'
        env['board'] = 'generic' + device
        # argument
        args = []
        arguments = target.find('TargetOption/DllOption/TargetDllArguments').text
        if arguments:
            arguments = arguments.split(',')
        else:
            arguments = []
        if '-MPU' in arguments:
            args.append('-mfloat-abi=hard')
        env['args'] = args
        # define
        defs = []
        defines = target.find('TargetOption/TargetArmAds/Cads/VariousControls/Define').text
        if defines:
            defines = defines.split(',')
            defines = list(filter(lambda _def: _def.strip(), defines))
        else:
            defines = []
        for define in defines:
            if not list(filter(lambda _def: _def in define, def_filter)):
                defs.append(define)
        defs.extend(extra_defs)
        env['defs'] = defs
        # include
        incs = []
        include_path = target.find('TargetOption/TargetArmAds/Cads/VariousControls/IncludePath').text
        for path in include_path.split(';'):
            if not list(filter(lambda inc: inc in path, inc_filter)):
                incs.append(path.replace('\\', '/').lstrip('./'))
        env['incs'] = incs
        # source
        srcs = []
        for group in target.find('Groups'):
            group_name = group.find('GroupName').text
            if not list(filter(lambda src: src in group_name, src_group_filter)):
                for file in group.find('Files'):
                    file_path = file.find('FilePath').text
                    if not list(filter(lambda src: src in file_path, src_file_filter)):
                        srcs.append(file_path.replace('\\', '/').lstrip('./'))
        env['srcs'] = srcs
    else:
        print('Error: unknow device %s' % (device))
        sys.exit(1)

def parse_keil5_project(path):
    envs = []
    print("-> Parse Keil5", path)
    tree = ET.parse(path)
    root = tree.getroot()
    for target in root.find('Targets'):
        env = {}
        # env name
        name = target.find('TargetName').text
        env['name'] = name.replace(' ', '-')
        # platformio
        toolset = target.find('ToolsetName').text
        if toolset == 'ARM-ADS':
            keil5_arm_ads(target, env)
            assembly_file_replace(env)
            envs.append(env)
        else:
            print('Error: unknow toolset %s' % (toolset))
            sys.exit(1)
    return envs

def keil4_mcs_51(target, env):
    env['platform'] = 'intel_mcs51'
    env['board'] = 'Generic8052'
    # argument
    arguments = target.find('TargetOption/DllOption/TargetDllArguments').text
    if arguments:
        arguments = arguments.split(',')
        print('Warn: arguments: ', (arguments))
    # define
    defs = []
    defines = target.find('TargetOption/Target51/C51/VariousControls/Define').text
    if defines:
        defines = defines.split(',')
        defines = list(filter(lambda _def: _def.strip(), defines))
    else:
        defines = []
    for define in defines:
        # if not list(filter(lambda _def: _def in define, def_filter)):
        defs.append(define)
    env['defs'] = defs
    # include
    incs = []
    include_path = target.find('TargetOption/Target51/C51/VariousControls/IncludePath').text
    for path in include_path.split(';'):
        # if not list(filter(lambda inc: inc in path, inc_filter)):
        incs.append(path.replace('\\', '/').lstrip('./'))
    env['incs'] = incs
    # source
    srcs = []
    for group in target.find('Groups'):
        # group_name = group.find('GroupName').text
        # if not list(filter(lambda src: src in group_name, src_group_filter)):
        for file in group.find('Files'):
            file_path = file.find('FilePath').text
            # if not list(filter(lambda src: src in file_path, src_file_filter)):
            srcs.append(file_path.replace('\\', '/').lstrip('./'))
    env['srcs'] = srcs

def parse_keil4_project(path):
    envs = []
    print("-> Parse Keil4", str(path))
    tree = ET.parse(path)
    root = tree.getroot()
    for target in root.find('Targets'):
        env = {}
        # env name
        name = target.find('TargetName').text
        env['name'] = name.replace(' ', '-')
        # platformio
        toolset = target.find('ToolsetName').text
        if toolset == 'MCS-51':
            keil4_mcs_51(target, env)
        else:
            print('Error: unknow toolset %s' % (toolset))
            sys.exit(1)
        envs.append(env)
    return envs

def convert_coding_utf8():
    print("-> Convert coding utf-8")
    curr = Path.cwd()
    p = re.compile('.*(c|h|txt|cxx|cpp|hpp|hxx|asm|S|s)$')
    for path in curr.rglob('*'):
        if path.is_file() and p.match(path.name):
            raw = path.read_bytes()
            result = chardet.detect(raw)
            encoding = result['encoding'].lower()
            if encoding in ['ascii', 'utf-8']:
                continue
            # print(path)
            # print(encoding)
            raw = raw.decode(encoding)
            raw = raw.encode('utf-8')
            path.write_bytes(raw)

def fixup_quirks():
    print("-> Fixup quirks")
    # alientek
    for file in list(Path('.').rglob('USMART/usmart_port.h')):
        print('fixup %s' % (file))
        text = file.read_text()
        text = text.replace('#ifndef uint32_t', '#ifndef _UINT32_T_DECLARED')
        file.write_text(text)

def main():
    global exec_path
    exec_path = Path(sys.argv[0]).parent.absolute()

    if len(sys.argv) != 2:
        print('Usage: %s path' % (sys.argv[0]))
        sys.exit(1)
    path = Path(sys.argv[1])
    if not path.is_dir():
        print('Error: %s is not dir' % (sys.argv[1]))
        sys.exit(1)
    os.chdir(path)
    if Path(PLATFORMIO_NAME).exists():
        print('Error: %s exists' % (PLATFORMIO_NAME))
        sys.exit(1)

    # keil5
    project = list(Path('.').rglob('*.uvprojx'))
    if project:
        if len(project) == 1:
            envs = parse_keil5_project(project[0])
            generate_platformio_ini(envs)
            convert_coding_utf8()
            fixup_quirks()
            sys.exit(0)
        else:
            print('Error: multiple project files exist:', project)
            sys.exit(1)
    
    # keil4
    project = list(Path('.').rglob('*.uvproj'))
    if project:
        if len(project) == 1:
            envs = parse_keil4_project(project[0])
            generate_platformio_ini(envs)
            convert_coding_utf8()
            sys.exit(0)
        else:
            print('Error: multiple project files exist:', project)
            sys.exit(1)

if __name__ == '__main__':
    main()